package com.duomi.lang;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@SuppressWarnings({"WeakerAccess", "unused"})
public class DuomiLang {
    /*
    Student.new @stu =;

    @stu.age 1 +; @age = ;

    {"larger than" println$1} {"smaller than" println$1} {1 2 >} if

     */

    public interface Predicate<T> {

        /**
         * Evaluates this predicate on the given argument.
         *
         * @param t the input argument
         * @return {@code true} if the input argument matches the predicate,
         * otherwise {@code false}
         */
        boolean test(T t);
    }

    private static final List<String> Keywords = Arrays.asList(
            "pop", "dup",
            "if", "while", "foreach", "map",
            "and", "or", "not"
    );

    private static final List<Class> BuiltinClasses = new ArrayList<>(Arrays.asList(
            Object.class,

            Math.class,
            Class.class,
            Date.class,
            System.class,

            Pattern.class,
            Matcher.class,

            File.class,

            String.class,
            Character.class,
            Boolean.class,
            Byte.class,
            Integer.class,
            Long.class,
            Float.class,
            Double.class,
            Number.class,

            Collections.class,
            Arrays.class,

            List.class,
            LinkedList.class,
            ArrayList.class,

            Map.class,
            HashMap.class,
            LinkedHashMap.class,

            Set.class,
            HashSet.class,
            LinkedHashSet.class,

            Thread.class,
            Runtime.class
    ));

    public interface Function {
        Object NO_RETURN = new Object();

        int getArity();

        Object invoke(VarContext context, Object[] arguments);
    }

    private static final Map<String, Function> BuiltinFunction = new HashMap<>();

    static {
        registerFunction("println$1", new PrintlnFunction(1));
        registerFunction("println$2", new PrintlnFunction(2));
        registerFunction("println$3", new PrintlnFunction(3));
        registerFunction("println$4", new PrintlnFunction(4));
        registerFunction("println$5", new PrintlnFunction(5));
        registerFunction("println$6", new PrintlnFunction(6));
        registerFunction("println$7", new PrintlnFunction(7));
        registerFunction("println$8", new PrintlnFunction(8));
        registerFunction("println$9", new PrintlnFunction(9));

        registerFunction("print$1", new PrintFunction(1));
        registerFunction("print$2", new PrintFunction(2));
        registerFunction("print$3", new PrintFunction(3));
        registerFunction("print$4", new PrintFunction(4));
        registerFunction("print$5", new PrintFunction(5));
        registerFunction("print$6", new PrintFunction(6));
        registerFunction("print$7", new PrintFunction(7));
        registerFunction("print$8", new PrintFunction(8));
        registerFunction("print$9", new PrintFunction(9));
    }

    public static void registerFunction(String name, Function function) {
        BuiltinFunction.put(name, function);
    }

    private static Function getFunction(String name) {
        return BuiltinFunction.get(name);
    }

    public interface Operator<T extends Number> {
        Object exec(T left, T right);
    }

    private static final Map<String, Operator<Integer>> intOperators = Collections.unmodifiableMap(new HashMap<String, Operator<Integer>>() {{
        //noinspection Convert2MethodRef
        put("+", (left, right) -> left + right);
        put("-", (left, right) -> left - right);
        put("*", (left, right) -> left * right);
        put("/", (left, right) -> left / right);
        put("%", (left, right) -> left % right);
        put(">", (left, right) -> left > right);
        put(">=", (left, right) -> left >= right);
        put("<", (left, right) -> left < right);
        put("<=", (left, right) -> left <= right);
        //noinspection NumberEquality
        put("==", (left, right) -> left == right);
    }});

    private static final Map<String, Operator<Double>> doubleOperators = Collections.unmodifiableMap(new HashMap<String, Operator<Double>>() {{
        //noinspection Convert2MethodRef
        put("+", (left, right) -> left + right);
        put("-", (left, right) -> left - right);
        put("*", (left, right) -> left * right);
        put("/", (left, right) -> left / right);
        put("%", (left, right) -> left % right);
        put(">", (left, right) -> left > right);
        put(">=", (left, right) -> left >= right);
        put("<", (left, right) -> left < right);
        put("<=", (left, right) -> left <= right);
        //noinspection NumberEquality
        put("==", (left, right) -> left == right);
    }});


    private static final Map<String, Class<?>> classCache = new HashMap<>();

    private static Class<?> findClass(String cls) throws ClassNotFoundException {
        if (classCache.containsKey(cls)) {
            return classCache.get(cls);
        }

        for (Class<?> builtinClass : BuiltinClasses) {
            if (cls.equals(builtinClass.getSimpleName())) {
                classCache.put(cls, builtinClass);
                return builtinClass;
            }
        }

        final Class<?> clazz = Class.forName(cls);
        classCache.put(cls, clazz);
        return clazz;
    }

    private static class Block {

        private final VarContext vars;
        private final List<Token> tokens;
        private final boolean debug;

        Block(boolean debug, VarContext vars, List<Token> tokens) {
            this.vars = vars;
            this.tokens = Collections.unmodifiableList(tokens);
            this.debug = debug;
        }

        Deque<Object> exec(String scope, Map<String, Object> localVars) {
            final Executor executor = new Executor(debug, vars);

            vars.pushLocalVarContext(scope);
            if (localVars != null) {
                vars.setVars(localVars);
            }
            executor.eval(tokens);
            vars.popLocalVarContext();

            return executor.exprStack;
        }
    }

    public static class VarContext {

        static class Context extends HashMap<String, Object> {
            private final String name;

            public Context(String name) {
                this.name = name;
            }

            @Override
            public String toString() {
                return "Context{" +
                        "name='" + name + '\'' +
                        "item=" + super.toString() +
                        '}';
            }
        }

        private final Context globalVars = new Context("GlobalContext");

        private final Deque<Context> varStack = new LinkedList<>();

        {
            varStack.add(globalVars);
        }

        public void pushLocalVarContext(String scope) {
            varStack.push(new Context("Local-" + scope));
        }

        public void popLocalVarContext() {
            assert varStack.size() > 1;
            varStack.pop();
        }

        public Object getVar(String name) {
            final Iterator<Context> varContextIt = varStack.descendingIterator();
            while (varContextIt.hasNext()) {
                final Map<String, Object> context = varContextIt.next();
                if (context.containsKey(name)) {
                    return context.get(name);
                }
            }

            throw new ExecuteException("unknown variable: " + name);
        }

        public void setVar(String name, Object value) {
            varStack.peekLast().put(name, value);
        }

        public void setVars(Map<String, Object> vars) {
            varStack.peekLast().putAll(vars);
        }

        public void setGlobalVars(Map<String, Object> vars) {
            globalVars.putAll(vars);
        }

        public void setGlobalVar(String name, Object value) {
            globalVars.put(name, value);
        }

        @Override
        public String toString() {
            return "VarContext{" +
                    "varStack=" + varStack +
                    '}';
        }
    }

    private static class Executor {

        private final boolean debug;
        private List<Token> remindTokens = Collections.emptyList();
        private final VarContext varContext;
        private final Deque<Object> exprStack = new LinkedList<>();

        private Executor(boolean debug, VarContext varContext) {
            this.varContext = varContext;
            this.debug = debug;
        }

        private Token peekToken() {
            return peekToken(0);
        }

        private Token peekToken(int step) {
            return remindTokens.get(step);
        }

        private Token tokAdvance() {
            return remindTokens.remove(0);
        }

        private <T> T tokExcept(TokenType type) {
            if (tokTryExcept(type)) {
                return tokAdvance().getValue();
            } else {
                throw new ExecuteException();
            }
        }

        private boolean tokTryExcept(TokenType type) {
            return peekToken().tokenType == type;
        }

        public void eval(List<Token> _tokens) {
            if (debug) {
                System.out.println("start executing, " + _tokens);
            }
            remindTokens = new ArrayList<>(_tokens);

            varContext.pushLocalVarContext("EvalSessionScope");
            try {
                while (!remindTokens.isEmpty()) {
                    final Token token = tokAdvance();
                    try {
                        switch (token.tokenType) {
                            case INT:
                            case DOUBLE:
                            case STRING:
                                push(token.value);
                                break;
                            case IDENTITY:
                                final String id = token.getValue();
                                if (Keywords.contains(id)) {
                                    handleKeyword(id);
                                } else {
                                    if (!tryHandleAsFunction(id)) {
                                        push(id);
                                    }
                                }
                                break;
                            case OPEN_BRACKET:
                                handleBlock();
                                break;
                            case OPERATOR:
                                handleOperator(token);
                                break;
                            case DOT:
                                handleDot(token.getValue());
                                break;
                            case VARIABLE:
                                handleGetVar(token.getValue());
                                break;
                            case EOF:
                                break;
                            default:
                                throw new ExecuteException("can handle token: " + token);
                        }
                    } catch (Exception e) {
                        System.out.println("executing failed, at: " + token + ", remind tokens: " + remindTokens);
                        if (e instanceof ExecuteException) {
                            throw ((ExecuteException) e);
                        } else {
                            throw new ExecuteException(e);
                        }
                    }

                    if (debug) {
                        System.out.println("<next tok> token = " + token + ", stack = " + exprStack + ", context = " + varContext);
                    }
                }
            } finally {
                varContext.popLocalVarContext();
            }

            if (debug) {
                System.out.println("<eval finish> stack = " + exprStack + ", context = " + varContext);
            }
        }

        private boolean tryHandleAsFunction(String id) {
            final Function function = getFunction(id);
            if (function == null) {
                return false;
            }

            final int arity = function.getArity();
            final Object[] arguments = new Object[arity];
            for (int i = arity - 1; i >= 0; i--) {
                arguments[i] = pop();
            }

            final Object result = function.invoke(varContext, arguments);
            if (result != Function.NO_RETURN) {
                push(result);
            }

            return true;
        }

        private void handleKeyword(String keyword) {

            switch (keyword) {
                case "pop":
                    pop();
                    break;
                case "dup":
                    push(exprStack.peekLast());
                    break;
                case "and":
                    handleAnd();
                    break;
                case "or":
                    handleOr();
                    break;
                case "not":
                    handleNot();
                    break;
                case "if":
                    handleIf();
                    break;
                case "while":
                    handleWhile();
                    break;
                case "map":
                    handleMap();
                    break;
                case "foreach":
                    handleForeach();
                    break;
                default:
                    throw new AssertionError("unsupported keywords: " + keyword);
            }
        }

        private void handleSetField() throws NoSuchFieldException, IllegalAccessException {
            final Object hostObj = pop();
            final String fieldName = (String) pop();
            final Object newValue = pop();

            final Field field = hostObj.getClass().getDeclaredField(fieldName);
            field.set(hostObj, newValue);
        }

        private void push(Object o) {
            exprStack.addLast(o);
        }

        private Object pop() {
            return exprStack.removeLast();
        }

        private void handleAnd() {
            final boolean left = (boolean) pop();
            final boolean right = (boolean) pop();
            push(left && right);
        }

        private void handleOr() {
            final boolean left = (boolean) pop();
            final boolean right = (boolean) pop();
            push(left || right);
        }

        private void handleNot() {
            final boolean left = (boolean) pop();
            push(!left);
        }

        private void handleMap() {
            final Iterable obj = ((Iterable) pop());
            final Block block = ((Block) pop());

            final List<Object> result = new ArrayList<>();

            int index = 0;
            for (Object item : obj) {
                final Map<String, Object> localVars = new HashMap<>();
                localVars.put("@it", item);
                localVars.put("@index", index);
                final Object newValue = block.exec("map", localVars).peekLast();
                result.add(newValue);
                index++;
            }

            push(result);
        }

        /**
         * {true-block} {false-block} {test-block} ifBlock
         */
        private void handleIf() {
            final Block testBock = (Block) pop();
            final Block falseBlock = (Block) pop();
            final Block trueBlock = (Block) pop();

            final boolean testResult = (boolean) testBock.exec("if-test-block", null).peekLast();
            if (testResult) {
                trueBlock.exec("if-true-block", null);
            } else {
                falseBlock.exec("if-false-block", null);
            }
        }

        /**
         * {body block} {test block} while
         */
        private void handleWhile() {
            final Block testBlock = (Block) pop();
            final Block bodyBlock = (Block) pop();

            while (true) {
                final Boolean testResult = (Boolean) testBlock.exec("while-test-block", null).peekLast();
                if (!testResult) {
                    break;
                }
                bodyBlock.exec("while-body-block", null);
            }
        }

        /**
         * {body} foreach
         */
        private void handleForeach() {
            final Iterable obj = ((Iterable) pop());
            final Block block = ((Block) pop());

            int index = 0;
            for (Object item : obj) {
                final Map<String, Object> localVars = new HashMap<>();
                localVars.put("@it", item);
                localVars.put("@index", index);
                block.exec("foreach-body-block", localVars);
                index++;
            }
        }

        private void handleGetVar(String varName) {
            if (tokTryExcept(TokenType.ASSIGN)) {
                tokAdvance();
                final Object value = pop();
                varContext.setVar(varName, value);
            } else {
                push(varContext.getVar(varName));
            }
        }

        private void handleDot(String id) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException, InstantiationException {
            final int arity = tokTryExcept(TokenType.ARITY) ? tokAdvance().getValue() : 0;
            final Object hostObj = pop();
            if (debug) {
                System.out.println("handleDot: id = " + id + ", arity = " + arity + ", hostObj = " + hostObj);
            }
            // class
            if (id.equals("class")) {
                assert arity == 0;
                assert hostObj instanceof String;

                final Class clz = findClass((String) hostObj);
                push(clz);
                return;
            }

            final Class[] argumentTypes = new Class[arity];
            final Object[] arguments = new Object[arity];

            for (int i = arity - 1; i >= 0; i--) {
                final Object arg = pop();
                arguments[i] = arg;
                argumentTypes[i] = arg.getClass();
            }

            // constructor
            if (id.equals("new")) {
                final Class clazz = findClass(((String) hostObj));
                //noinspection unchecked
                final Constructor<?> constructor = clazz.getDeclaredConstructor(argumentTypes);
                final Object obj = constructor.newInstance(arguments);
                push(obj);
                return;
            }


            // method
            final Method method = getDeclaredMethod(id, hostObj, argumentTypes);
            if (method != null) {
                final Object result = method.invoke(hostObj, arguments);
                if (!(result instanceof Void)) {
                    push(result);
                    return;
                }
            }

            // field
            if (arity == 0) {
                final Field field = hostObj.getClass().getDeclaredField(id);
                if (tokTryExcept(TokenType.ASSIGN)) {
                    tokAdvance();
                    final Object newValue = pop();
                    field.set(hostObj, newValue);
                    return;
                } else {
                    push(field.get(hostObj));
                    return;
                }
            }

            throw new ExecuteException("unknown dot expression, hostObj = " + hostObj + ", id = " + id + ", args = " + Arrays.toString(argumentTypes));
        }

        private Method getDeclaredMethod(String id, Object hostObj, Class[] argumentTypes) {

            final Class<?> clazz = hostObj.getClass();
            try {
                return clazz.getDeclaredMethod(id, argumentTypes);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }

            // generic method
            final Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {
                if (method.getName().equals(id) && method.getParameterCount() == argumentTypes.length) {
                    return method;
                }
            }

            return null;
        }

        private void handleOperator(Token token) {
            final Object right = pop();
            final Object left = pop();

            assert left instanceof Number && right instanceof Number;

            if (left instanceof Integer && right instanceof Integer) {
                final Object result = intOperators.get(token.<String>getValue()).exec(((Integer) left), ((Integer) right));
                push(result);
                return;
            }

            final Object result = doubleOperators.get(token.<String>getValue()).exec(((Number) left).doubleValue(), ((Number) right).doubleValue());
            push(result);
        }

        private void handleBlock() {
            final List<Token> blockTokens = new ArrayList<>();

            int openingBlockLevel = 1;
            while (true) {
                final Token token = tokAdvance();
                if (token.tokenType == TokenType.CLOSE_BRACKET) {
                    openingBlockLevel -= 1;
                    if (openingBlockLevel == 0) {
                        break;
                    }
                }

                if (token.tokenType == TokenType.OPEN_BRACKET) {
                    openingBlockLevel += 1;
                }

                blockTokens.add(token);
            }

            final Block block = new Block(debug, varContext, blockTokens);
            push(block);
        }
    }

    public enum TokenType {
        OPEN_BRACKET,
        CLOSE_BRACKET,
        DOT,
        STRING,
        INT,
        DOUBLE,
        IDENTITY,
        ARITY,
        OPERATOR,
        VARIABLE,
        ASSIGN,
        EOF,
    }

    public static final class Token {
        public final TokenType tokenType;
        public final Object value;

        public Token(TokenType tokenType, Object value) {
            this.tokenType = tokenType;
            this.value = value;
        }

        public <T> T getValue() {
            //noinspection unchecked
            return ((T) value);
        }

        @Override
        public String toString() {
            return "Token{" +
                    "tokenType=" + tokenType +
                    ", value='" + value + "'" +
                    '}';
        }
    }

    public static final class Lexer {

        private final String source;
        private final List<Token> tokens = new ArrayList<>();
        private int index;
        private boolean hasLex;

        public Lexer(String source) {
            this.source = source;
            this.index = 0;
            this.hasLex = false;
        }

        public List<Token> getTokens() {
            if (!hasLex) {
                lex();
                hasLex = true;
            }
            return tokens;
        }

        private void addToken(TokenType tokenType, Object value) {
            tokens.add(new Token(tokenType, value));
        }

        private char lookAhead(int step) {
            return source.charAt(index + step);
        }

        private char peek() {
            return source.charAt(index);
        }

        private boolean isEof(int step) {
            return index + step >= source.length();
        }

        private String advance(int step) {
            int start = this.index;
            this.index += step;
            int end = this.index;
            return source.substring(start, end);
        }

        private void exceptAtLeast(int minCount, Predicate<Character> characterPredicate) {
            if (!tryExceptAtLeast(minCount, characterPredicate)) {
                System.out.println(source);
                for (int i = 0; i < index; i++) {
                    System.out.print('-');
                }
                System.out.println('|');
                throw new SyntaxException("except minCount: " + minCount + ", pattern: " + characterPredicate + ", at index: " + index);
            }
        }

        private boolean tryExceptAtLeast(int minCount, Predicate<Character> characterPredicate) {
            int step = 0;
            while (!isEof(step) && characterPredicate.test(lookAhead(step))) {
                step++;
            }
            return step >= minCount;
        }

        private void lex() {
            final int length = source.length();
            while (index < length) {
                try {

                    final char c = peek();
                    if (Character.isSpaceChar(c)) {
                        lexSpace();
                        continue;
                    }
                    switch (c) {
                        case '>':
                        case '<':
                        case '=':
                            lexCompareOperator(c);
                            break;
                        case '"':
                            lexString();
                            break;
                        case '+':
                        case '-':
                        case '0':
                        case '1':
                        case '2':
                        case '3':
                        case '4':
                        case '5':
                        case '6':
                        case '7':
                        case '8':
                        case '9':
                            if (!tryLexNumber()) {
                                advance(1);
                                addToken(TokenType.OPERATOR, c + "");
                            }
                            break;
                        case '*':
                        case '%':
                            advance(1);
                            addToken(TokenType.OPERATOR, c + "");
                            break;
                        case '{':
                            advance(1);
                            addToken(TokenType.OPEN_BRACKET, "(");
                            break;
                        case '}':
                            advance(1);
                            addToken(TokenType.CLOSE_BRACKET, ")");
                            break;
                        case '.':
                            advance(1);
                            addToken(TokenType.DOT, consumeId());
                            break;
                        case '#':
                            lexComment();
                            break;
                        case '/':
                            if (!isEof(1) && Character.isDigit(lookAhead(1))) {
                                lexArity();
                            } else {
                                advance(1);
                                addToken(TokenType.OPERATOR, c + "");
                            }
                            break;
                        case ';': // ignore
                            advance(1);
                            break;
                        case '@':
                            lexVariable();
                            break;
                        default:
                            lexId();
                            break;
                    }
                } catch (Exception e) {
                    System.out.println("Lex Phase exception: at index: " + index);
                    System.out.println(source);
                    for (int i = 0; i < index; i++) {
                        System.out.print('_');
                    }
                    System.out.println("|️");
                    if (e instanceof SyntaxException) {
                        throw e;
                    } else {
                        throw new SyntaxException(e);
                    }
                }
            }

            addToken(TokenType.EOF, "");
        }

        private void lexVariable() {
            advance(1);
            final String variableName = consumeId();
            addToken(TokenType.VARIABLE, variableName);
        }

        private void lexCompareOperator(char c) {
            if (c == '=') {
                advance(1);
                if (tryExceptAtLeast(1, test -> test == '=')) {
                    advance(1);
                    addToken(TokenType.OPERATOR, "==");
                } else {
                    addToken(TokenType.ASSIGN, "=");
                }
            } else {
                advance(1);
                if (tryExceptAtLeast(1, test -> test == '=')) {
                    advance(1);
                    addToken(TokenType.OPERATOR, c + "=");
                } else {
                    addToken(TokenType.OPERATOR, c + "=");
                }
            }
        }

        private void lexComment() {
            advance(1);
            int step = 0;
            while (!isEof(step) && lookAhead(step) != '\n') {
                step++;
            }
            advance(step);
        }

        private void lexId() {
            addToken(TokenType.IDENTITY, consumeId());
        }

        private String consumeId() {
            exceptAtLeast(1, DuomiLang::isIdentifierStart);
            int step = 0;
            while (!isEof(step) && isIdentifierStart(lookAhead(step))) {
                step++;
            }
            while (!isEof(step) && isIdentifierPart(lookAhead(step))) {
                step++;
            }
            return advance(step);
        }

        private void lexSpace() {
            int step = 0;
            while (!isEof(step) && Character.isSpaceChar(lookAhead(step))) {
                step++;
            }
            advance(step);
        }

        private boolean tryLexNumber() {
            int sign = 1;
            final char peek = peek();
            if (peek == '+' || peek == '-') {
                if (!tryExceptAtLeast(1, Character::isDigit)) {
                    return false;
                } else {
                    advance(1);
                    sign = peek == '-' ? -1 : 1;
                }
            }

            exceptAtLeast(1, Character::isDigit);

            final String integerPart;
            {
                int step = 0;
                while (!isEof(step) && Character.isDigit(lookAhead(step))) {
                    step++;
                }
                integerPart = advance(step);
            }

            if (peek() == '.') {
                advance(1);
                exceptAtLeast(1, Character::isDigit);

                int step = 0;
                while (!isEof(step) && Character.isDigit(lookAhead(step))) {
                    step++;
                }
                final String decimalPart = advance(step);

                final double value = sign * Double.parseDouble(integerPart + "." + decimalPart);
                addToken(TokenType.DOUBLE, value);
                return true;
            }

            addToken(TokenType.INT, sign * Integer.parseInt(integerPart));

            return true;
        }

        private void lexArity() {
            advance(1); // consume /
            exceptAtLeast(1, Character::isDigit);

            int step = 0;
            while (!isEof(step) && Character.isDigit(lookAhead(step))) {
                step++;
            }

            final String arity = advance(step);
            addToken(TokenType.ARITY, Integer.parseInt(arity));
        }

        private void lexString() {

            advance(1);

            int step = 0;
            while (true) {
                if (isEof(step)) {
                    throw new SyntaxException("UnClosed String");
                }

                final char c = lookAhead(step);
                final char last = lookAhead(step - 1);
                step++;
                if (c == '"' && last != '\\') {
                    break;
                }
            }

            final String capture = advance(step);

            addToken(TokenType.STRING, capture.substring(0, capture.length() - 1));
        }
    }

    private static boolean isIdentifierStart(char c) {
        return '_' == c || '$' == c || Character.isLetter(c);
    }

    private static boolean isIdentifierPart(char c) {
        return "_$-".indexOf(c) >= 0 || Character.isLetterOrDigit(c);
    }

    public static class SyntaxException extends RuntimeException {

        public SyntaxException() {
        }

        public SyntaxException(String message) {
            super(message);
        }

        public SyntaxException(String message, Throwable cause) {
            super(message, cause);
        }

        public SyntaxException(Throwable cause) {
            super(cause);
        }

        public SyntaxException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
        }
    }

    public static class ExecuteException extends RuntimeException {

        public ExecuteException() {
        }

        public ExecuteException(String message) {
            super(message);
        }

        public ExecuteException(String message, Throwable cause) {
            super(message, cause);
        }

        public ExecuteException(Throwable cause) {
            super(cause);
        }

        public ExecuteException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
        }
    }

    private final VarContext varContext = new VarContext();

    public void export(String name, Object value) {
        varContext.setGlobalVar(name, value);
    }

    public VarContext getContext() {
        return varContext;
    }

    public void addBuiltinClass(Class clz) {
        DuomiLang.BuiltinClasses.add(clz);
    }

    private List<Token> lex(String source) {
        return new Lexer(source).getTokens();
    }

    private Deque<Object> eval(boolean debug, List<Token> tokens) {
        final Executor executor = new Executor(debug, varContext);
        executor.eval(tokens);
        return executor.exprStack;
    }

    public Deque<Object> eval(String source) {
        return eval(false, source);
    }

    public Deque<Object> eval(boolean debug, String source) {
        final List<Token> tokens = lex(source);
        return eval(debug, tokens);
    }

    static class PrintlnFunction implements Function {
        private final int arity;

        public PrintlnFunction(int arity) {
            this.arity = arity;
        }

        @Override
        public int getArity() {
            return arity;
        }

        @Override
        public Object invoke(VarContext context, Object[] arguments) {
            System.out.println("Duomi->: " + Arrays.toString(arguments));
            return NO_RETURN;
        }
    }

    static class PrintFunction implements Function {
        private final int arity;

        public PrintFunction(int arity) {
            this.arity = arity;
        }

        @Override
        public int getArity() {
            return arity;
        }

        @Override
        public Object invoke(VarContext context, Object[] arguments) {
            System.out.print("Duomi->: " + Arrays.toString(arguments));
            return NO_RETURN;
        }
    }
}
